package cs340.stormtroopers.hypeerweb;

import gui.GUIFacade;

import java.io.IOException;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Random;

import phase6.GlobalLocalObjectIds;
import phase6.LocalObjectId;
import phase6.ObjectDB;
import phase6.SegmentRunner;
import cs340.stormtroopers.database.HyPeerWebDatabase;

/**
 * The in-memory HyPeerWeb <br>
 * 
 * <pre>
 * <b>Domain</b>
 * 		nodes		: Map&#60Web ID, Node&#62
 * 		database	: HyPeerWebDatabase
 * 		random		: Random
 * 		guis		: Set&#60GUIFacade&#62
 * 		
 * <b>Invariant</b>
 * 		nodes is a non-null Map which maps a Web ID to a Node object.
 * 		database is a non-null singleton object that connects to a database.
 * 		random is a non-null Random object.
 * `	guis is a non-null Set of GUIFacades connected to the HyPeerWeb.
 * </pre>
 * 
 * @author Andy Morrow
 * @author Brad Spendlove
 * @author Daniel Howard
 * @author James Gurr
 * @author William Highland
 */
public class HyPeerWeb implements Serializable
{
	/**
	 * 
	 */
	private static final long serialVersionUID = 6249955520073874042L;

	/**
	 * Singleton object with which to interact with the HyPeerWeb.
	 */
	private static HyPeerWeb singleton = null;

	/**
	 * Local Id for the HyPeerWeb
	 */
	public static final LocalObjectId SINGLETON_OBJECT_ID = new LocalObjectId(phase6.GlobalLocalObjectIds.HYPEERWEB_SINGLETON);

	/**
	 * Map containing the Nodes in the HyPeerWeb.
	 */
	private HashMap<Integer, Node> nodes;
	/**
	 * Random object for generating random numbers.
	 */
	private final Random random = new Random();

	/**
	 * Set of GUIs connected to the HyPeerWeb
	 */
	private HashSet<GUIFacade> guis;
	
	private boolean nodesShouldSerializeToProxies = true;
	
	public boolean getNodesShouldSerializeToProxies()
	{
		return nodesShouldSerializeToProxies;
	}
	
	public boolean setNodesShouldSerializeToProxies(boolean val)
	{
		return nodesShouldSerializeToProxies = val;
	}

	/**
	 * Private constructor for creation of the singleton object.
	 * 
	 * @pre none
	 * @post A new HyPeerWeb object is created for the singleton. Domain is
	 *       initialized.
	 */
	protected HyPeerWeb(boolean createDatabase)
	{
		nodes = new HashMap<Integer, Node>();
		guis = new HashSet<GUIFacade>();
		if (createDatabase)
		{
			try
			{
				HyPeerWebDatabase.initHyPeerWebDatabase();
				HyPeerWebDatabase.getSingleton();
			}
			catch (IOException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			catch (ClassNotFoundException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			catch (SQLException e)
			{
				System.err
						.println("Could not load database. Perhaps it was empty?");
			}
		}
	}

	/**
	 * Add a new complete node to the HyPeerWeb. Does not manage connections.
	 * 
	 * @param node
	 *            The new Node to add to the HyPeerWeb.
	 * 
	 * @pre nodes.size >= 0.<br/>
	 *      node != null.
	 * @post The node is added to the HyPeerWeb.<br/>
	 *       nodes' size is increased by one.<br/>
	 *       The new Node is accessible by its webId value.
	 */
	public boolean addNode(Node node)
	{
		nodes.put(node.getWebIdValue(), node);
		
		//Updatin ma guis
		for (GUIFacade g : guis)
			g.nodeAdded(node.getWebIdValue());
		
		return true;
	}

	/**
	 * Adds a new node to the HyPeerWeb.
	 * 
	 * @param newNode
	 *            The new node to add to the HyPeerWeb.
	 * @param startNode
	 *            The starting point for which to scan the HyPeerWeb for the
	 *            insertion point. If startNode is null, it chooses a random
	 *            start point.
	 * 
	 * @pre nodes.size >= 0.<br/>
	 *      If the hypercube is incomplete: Down pointers and up pointers and
	 *      possibly (inverse) surrogate folds will exist.<br/>
	 *      Else: No up or down pointers and no surrogate folds will exist.
	 * @post The size of nodes will increase by 1.<br/>
	 *       The new Node will have the highest Web ID in the HyPeerWeb.<br/>
	 *       The new Node will be connected into the HyPeerWeb in line with the
	 *       use cases.
	 */
	public void addToHyPeerWeb(Node newNode, Node startNode)
	{
		if (size() == 0 && startNode == null)
		{
			System.out
					.println("in addToHyPeerWeb: There were 0 nodes in this segment and I'm adding node 0");
			newNode.setWebId(new WebId(0));
			newNode.setFold(newNode);
			newNode.addMeToMyHyPeerWeb();
		}
		else
		{
			if (startNode == null || startNode == Node.NULL_NODE)
			{
				startNode = getRandomNode();
			}
			startNode.addToHyPeerWeb(newNode);
		}
	}

	public Node getRandomNode()
	{
		Node[] nodeArray = nodes.values().toArray(new Node[0]);
		return nodeArray[random.nextInt(nodeArray.length)];
	}

	/**
	 * Adds a new node to the HyPeerWeb.
	 * 
	 * @param newNode
	 *            The new node to add to the HyPeerWeb.
	 * @param startNode
	 *            The starting point for which to scan the HyPeerWeb for the
	 *            insertion point. If startNode is null, it chooses a random
	 *            start point.
	 * 
	 * @pre nodes.size >= 0.<br/>
	 *      If the hypercube is incomplete: Down pointers and up pointers and
	 *      possibly (inverse) surrogate folds will exist.<br/>
	 *      Else: No up or down pointers and no surrogate folds will exist.
	 * @post The size of nodes will increase by 1.<br/>
	 *       The new Node will have the highest Web ID in the HyPeerWeb.<br/>
	 *       The new Node will be connected into the HyPeerWeb in line with the
	 *       use cases.
	 */
	public boolean addAnotherNode()
	{
		Node newNode = new Node(42);
		addToHyPeerWeb(newNode, null);
		addNode(newNode);
		return true;
	}

	/**
	 * Remove a node from the HyPeerWeb.
	 * 
	 * @param nodeToDelete
	 *            The node which is to be removed from the HyPeerWeb.
	 * 
	 * @pre nodes.size >= 2.
	 * @post The size of the HyPeerWeb will be reduced by 1.<br/>
	 *       If nodeToDelete.webId < nodes.size - 1: The node with the highest
	 *       Web ID (the replacing Node) will take the place of the Node with
	 *       the Web ID equaling webId (leaving Node).<br/>
	 *       Else: The leaving Node will be removed from the HyPeerWeb with no
	 *       Node replacing it.<br/>
	 *       The Nodes formerly connected to the replacing Node will adjust
	 *       their connections in line with the use cases.
	 */
	public void removeFromHyPeerWeb(Node nodeToDelete)
	{
		nodeToDelete.removeFromHyPeerWeb();
	}

	/**
	 * Removes a node with the specified Web ID from the HyPeerWeb.
	 * 
	 * @param webID
	 *            The Web Id corresponding to the node to be removed.
	 * 
	 * @pre nodes.size >= 2.
	 * @post The size of the HyPeerWeb will be reduced by 1.<br/>
	 *       If webId < nodes.size - 1: The node with the highest Web ID (the
	 *       replacing Node) will take the place of the Node with the Web ID
	 *       equaling webId (leaving Node).<br/>
	 *       Else: The leaving Node will be removed from the HyPeerWeb with no
	 *       Node replacing it.<br/>
	 *       The Nodes formerly connected to the replacing Node will adjust
	 *       their connections in line with the use cases.
	 */
	public void removeNodeByReference(int webId)
	{
		Node n = nodes.get(webId);
		
		if(n != null && n != Node.NULL_NODE)
			removeFromHyPeerWeb(n);
	}

	/**
	 * Removes a node from the HyPeerWeb. Does not manage connections.
	 * 
	 * @param node
	 *            The node to be removed from the HyPeerWeb.
	 * 
	 * @pre A HyPeerWeb of 0+ nodes.
	 * @post The number of nodes in the HyPeerWeb will be reduced by 1, if the
	 *       passed node is a member of the HyPeerWeb.<br/>
	 *       The passed Node will no longer be accessible by the HyPeerWeb, if
	 *       that node was a member before the removal.
	 */
	public boolean removeNode(Node node)
	{
		nodes.remove(node.getWebIdValue());
		for (GUIFacade g : guis)
			g.nodeRemoved(node.getWebIdValue());
		return true;
	}

	/**
	 * Clears all the Nodes from the HyPeerWeb.
	 * 
	 * @pre nodes.size >= 0.
	 * @post nodes.size == 0.
	 */
	public void clear()
	{
		nodes.clear();
		for (GUIFacade g : guis)
			g.webCleared();
	}

	/**
	 * Determines whether the passed node is a member of the HyPeerWeb.
	 * 
	 * @param node
	 *            The node to search for in the HyPeerWeb.
	 * @return True when the passed node is present, and false otherwise.
	 * 
	 * @pre node will not be null.
	 * @post See return.
	 */
	public boolean contains(Node node)
	{
		return nodes.containsKey(node.getWebId().hashCode());
	}

	/**
	 * Returns the database object held by the HyPeerWeb.
	 * 
	 * @return The HyPeerWebDatabase object held by the HyPeerWeb.
	 * 
	 * @pre none
	 * @post See return.
	 */
	public HyPeerWebDatabase getHyPeerWebDatabase()
	{
		try
		{
			return HyPeerWebDatabase.getSingleton();
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		catch (ClassNotFoundException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		catch (SQLException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * Returns the node with the passed webId.
	 * 
	 * @param i
	 *            The webId of the node being sought after.
	 * @return The node with the passed webId or null if no such node exists.
	 * 
	 * @pre none
	 * @post See return.
	 */
	public Node getNode(int i)
	{
		return nodes.get(i);
	}

	/**
	 * Returns the singleton object for the HyPeerWeb.
	 * 
	 * @return The singleton object for the HyPeerWeb.
	 * 
	 * @pre none
	 * @post See return.
	 */
	public static HyPeerWeb getSingleton()
	{
		if (singleton == null)
			singleton = new HyPeerWeb(true);

		return singleton;
	}

	/**
	 * Returns the LocalObjectId for the HyPeerWeb.
	 * 
	 * @return The LocalObjectId
	 * 
	 * @pre none
	 * @post See return.
	 */
	public static LocalObjectId getLocalObjectId()
	{
		return SINGLETON_OBJECT_ID;
	}

	/**
	 * Reloads the information held in the database into the HyPeerWeb.<br/>
	 * Removes any unsaved data in the process. Reloads from the default
	 * database.
	 * 
	 * @pre A HyPeerWeb of 0+ nodes representing the current state of the
	 *      HyPeerWeb.
	 * @post A HyPeerWeb of 0+ nodes reflecting the state of the HyPeerWeb at
	 *       the time of the last save operation.<br/>
	 *       If some error is raised when accessing the database, no change will
	 *       occur to the HyPeerWeb.
	 */
	public void reload()
	{
		reload(null);
	}

	/**
	 * Reloads the information held in the passed database into the HyPeerWeb.
	 * Removes any unsaved data in the process.
	 * 
	 * @param dbName
	 *            The database from which to load the data for the reloaded
	 *            HyPeerWeb.
	 * 
	 * @pre A HyPeerWeb of 0+ nodes representing the current state.
	 * @post A HyPeerWeb of 0+ nodes reflecting the state of the HyPeerWeb at
	 *       the time of the last save operation performed on the passed
	 *       database.<br/>
	 *       If some error is raised when accessing the database, no change will
	 *       occur to the HyPeerWeb.
	 */
	public void reload(java.lang.String dbName)
	{
		try
		{
			if (dbName == null)
				HyPeerWebDatabase.initHyPeerWebDatabase();
			else
				HyPeerWebDatabase.initHyPeerWebDatabase(dbName);
			HyPeerWebDatabase.getSingleton();
		}
		catch (IOException i)
		{
			System.out.println(i);
			return;
		}
		catch (ClassNotFoundException c)
		{
			System.out.println(c);
			return;
		}
		catch (SQLException s)
		{
			// System.out.println(s);
			System.err
					.println("Could not reload from database. Perhaps it was empty?");
			return;
		}

		nodes.clear(); // it won't clear the map if it can't load the database

		HyPeerWebBuilder builder;
		try
		{
			builder = new HyPeerWebBuilder(
					HyPeerWebDatabase.getSingleton().getSimplifiedNodeDomainSet());
			nodes.putAll(builder.getNodeMap());
		}
		catch (SQLException e)
		{
			System.err
					.println("Could not reload from database. Perhaps it was empty?");
			return;
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		catch (ClassNotFoundException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void clearGUIs()
	{
		guis.clear();
	}
	
	/**
	 * Shuts down the HyPeerWeb.
	 * 
	 * @pre None
	 * @post the HyPeerWeb's state has been saved and it is ready to be
	 *       destroyed
	 */
	public void close()
	{
		ObjectDB.getSingleton().save("ObjectDB");
		System.out.println("The database has been saved to disk ");
		
		
		
		saveToDatabase();
	}

	/**
	 * Saves the current state of the HyPeerWeb to the database.
	 * 
	 * @pre The database within the HyPeerWeb will hold the data for the
	 *      HyPeerWeb from the last save event, or nothing if no such event
	 *      occurred.
	 * @post The database will hold the information for the current state of the
	 *       HyPeerWeb, unless an error occurred.<br/>
	 *       If an error occurred, no changes will be made to the database, and
	 *       a stack trace will be printed out.
	 */
	public void saveToDatabase()
	{
		try
		{
			HyPeerWebDatabase database = HyPeerWebDatabase.getSingleton();
			database.clear();
			database.saveNodes(nodes.values());
		}
		catch (SQLException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		catch (ClassNotFoundException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Returns the number of nodes in the HyPeerWeb.
	 * 
	 * @return The number of nodes in the HyPeerWeb.
	 * 
	 * @pre none
	 * @post See return.
	 */
	public int size()
	{
		return nodes.size();
	}

	/**
	 * Connects a GUIFacade to the HyPeerWeb.
	 * 
	 * @param g
	 *            The GUIFacade to be connected.
	 * 
	 * @pre 0+ GUIFacades will be connected.
	 * @post If g is not already connected: guis.size will increase by 1.<br/>
	 *       else: guis.size will not change.
	 */
	public void addGUI(GUIFacade g)
	{
		guis.add(g);
		g.printToTracePanel("GUI connected!\n");
		System.out.println("GUI connected!");
	}

	/**
	 * Disconnects a GUIFacade from the HyPeerWeb.
	 * 
	 * @param g
	 *            The GUIFacade to be disconnected.
	 * 
	 * @pre 0+ GUIFacades will be connected.
	 * @post If g is not already connected: guis.size will not change.<br/>
	 *       else: guis.size will decrease by 1.
	 */
	public void removeGUI(GUIFacade g)
	{
		guis.remove(g);
		System.out.println("GUI disconnected");
	}

	/**
	 * Sends a broadcast message across the HyPeerWeb.
	 * 
	 * @param startWebId
	 *            The Web ID of the node to initiate the broadcast.
	 * @param visitor
	 *            The BroadcastVisitor for sending the message.
	 * @param parameters
	 *            The Parameters held by the visitor
	 * 
	 * @pre nodes.size > 0.<br/>
	 *      0 <= startWebId < nodes.size.<br/>
	 *      visitor != null.<br/>
	 *      parameters != null.
	 * @post Every node in the HyPeerWeb will receive the message carried by the
	 *       visitor.</br> Every GUI connected to the HyPeerWeb will receive
	 *       trace information from the visitor.
	 */
	public void remoteBroadCast(int startWebId, BroadcastVisitor visitor,
			Parameters parameters)
	{
		System.out.println("REMOTE BROADCAST");
		visitor.setGUIs(guis);
		nodes.get(startWebId).accept(visitor, parameters);
		// for(GUIFacade g : guis)
		// g.printToTracePanel(visitor.getStringPath());
	}

	/**
	 * Sends a message between Nodes in the HyPeerWeb.
	 * 
	 * @param from
	 *            The Node sending the message.
	 * @param visitor
	 *            The SendVisitor carrying the message.
	 * @param parameters
	 *            The Parameters being carried by the SendVisitor.
	 * 
	 * @pre nodes.size > 0.<br/>
	 *      0 <= startWebId < nodes.size.<br/>
	 *      visitor != null.<br/>
	 *      parameters != null.
	 * @post The receiving node will receive the sent message. The connected
	 *       GUIs will receive the trace information from the visitor.
	 */
	public void remoteSend(int from, SendVisitor visitor, Parameters parameters)
	{
		System.out.println("REMOTE SEND");
		visitor.setGUIs(guis);
		nodes.get(from).accept(visitor, parameters);
		// for(GUIFacade g: guis)
		// g.printToTracePanel(visitor.getStringPath());
	}

	public HashMap<Integer, Node> getNodeMap()
	{
		return nodes;
	}

	// use HyPeerWeb singleton to print to its guis tracepanels
	public void printToAllTracePanels(String msg)
	{
		// TODO add to Proxy???

		for (GUIFacade g : guis)
			g.printToTracePanel(msg);

		// TODO actually use it in the different visitors
	}
	
	public String toString()
	{
		StringBuilder sb = new StringBuilder();
		for(Node n : nodes.values())
		{
			if(n instanceof NodeProxy)
			{
				sb.append("PROXY:");
			}
			sb.append("[" + n.toString() + "] \n");
		}
		return sb.toString();
	}
	/**
	 * Leave the currently running session and attempt to migrate my nodes to another segment
	 * 
	 * @return
	 */
	public void leaveSession()
	{
		nodesShouldSerializeToProxies = false;
		ObjectDB.getSingleton().dump();
		
		SegmentRunner segment = (SegmentRunner) ObjectDB.getSingleton().getValue(
				new LocalObjectId(GlobalLocalObjectIds.SEGMENTRUNNER_SINGLETON));
		
		segment.moveNodesToAnotherSegment(nodes);
		System.exit(0);
	}
}
